package ipmanager;

import ipmanager.common.BinaryNumber;
import ipmanager.common.IP;
import ipmanager.common.IPClass;
import ipmanager.common.IPFormat;

import java.util.ArrayList;
import java.util.List;

public class SubnettingIP extends ClassfullIP {

	private List<IP> redAddressesIP;

	private List<IP> broadcastsIP;

	private List<IP> availablesIP;

	/**
	 * Constructor de SubnettingIP
	 * 
	 * @param addressIP
	 * @param mask
	 * @param ipFormat
	 * @throws Exception
	 * @throws Exception
	 */
	public SubnettingIP(String addressIP, String mask, String ipFormat,
			boolean showAvailablesIP) throws Exception {

		super(addressIP, false);

		this.validMask = isValidMask(mask);

		if (this.validMask) {

			this.classIP = calculateClassIP(ip.getBinIP());

			this.mask = new IP(mask, ipFormat);

			this.ip = new IP(addressIP, ipFormat);

			if (classIP.equalsIgnoreCase(IPClass.CLASS_C)) {
				this.redAddressesIP = calculateRedAddresses(this.ip, this.mask,
						this.classIP);
				this.broadcastsIP = calculateBroadcasts(this.ip, this.mask,
						this.classIP);
				if (showAvailablesIP)
					this.availablesIP = calculateAvailablesIP(
							this.redAddressesIP, this.broadcastsIP);
			}

		}
	}

	/**
	 * Calcula las posibles direcciones de red
	 * 
	 * @param ip
	 * @param classIP
	 * @return redAddressesIP
	 * @throws Exception
	 */
	private List<IP> calculateRedAddresses(IP ip, IP mask, String classIP)
			throws Exception {

		return calculateIPs(ip, mask, classIP, "0");

	}

	/**
	 * Calcula las posibles direcciones broadcast
	 * 
	 * @param ip
	 * @param mask
	 * @param classIP
	 * @return broadcastsIP
	 * @throws Exception
	 */
	private List<IP> calculateBroadcasts(IP ip, IP mask, String classIP)
			throws Exception {

		return calculateIPs(ip, mask, classIP, "1");

	}

	/**
	 * Calcula la coleccion de direcciones IP (direcciones de red y broadcast)
	 * 
	 * @param ip
	 * @param mask
	 * @param classIP
	 * @param binAfter
	 * @return collectionIP
	 * @throws Exception
	 */
	private List<IP> calculateIPs(IP ip, IP mask, String classIP,
			String binAfter) throws Exception {

		// Obtengo cantidad de 0 (ceros) de la mascara
		int zeroCount = mask.getZeroCount();

		// Obtengo la cantidad de bits de la parte subred+hosts de la mascara
		int localPartBits = 0;
		if (classIP.equals(IPClass.CLASS_C)) {
			localPartBits = 8;
		}

		// Calculo la cantidad de bits para los hosts y la subred
		int subredBitsCount = localPartBits - zeroCount;

		// Obtengo las combinaciones binarias usando subredBitsCount
		List<BinaryNumber> binCombinations = getBinCombinations(subredBitsCount);

		// Armo las distintas direcciones IP usando binCombinations (menos la
		// 1ra y ultima, 000...0 y 111...1)
		List<IP> collectionIP = new ArrayList<IP>();

		// Armo las combinaciones de collectionIP
		int pos = 0;
		for (BinaryNumber binNumber : binCombinations) {

			// Si no es la primera o ultima
			if ((pos != 0) && (pos != binCombinations.size() - 1)) {

				IP newIP = buildRedAddressIP(binNumber, binAfter);
				collectionIP.add(newIP);
			}
			pos++;
		}

		return collectionIP;
	}

	/**
	 * Retorna las direcciones IP disponibles
	 * 
	 * @param redAddressesIP
	 * @param broadcastsIP
	 * @return availablesIP
	 * @throws Exception
	 */
	private List<IP> calculateAvailablesIP(List<IP> redAddressesIP,
			List<IP> broadcastsIP) throws Exception {

		List<IP> availablesIP = new ArrayList<IP>();

		// Recorro todas las direcciones de red y broadcast
		for (int i = 0; i < redAddressesIP.size(); i++) {

			// Obtengo la direccion de red y broadcast i
			IP redAddressIP = redAddressesIP.get(i);
			IP broadcastIP = broadcastsIP.get(i);

			IP oneIP = new IP("0.0.0.1", IPFormat.BINARY);
			IP availableIP = new IP(redAddressIP.getBinIP(), IPFormat.BINARY);
			availableIP = availableIP.addOperation(oneIP);

			// Mientras la direccion IP available no sea broadcast, sumar 1
			while (!availableIP.getBinIP().equals((broadcastIP.getBinIP()))) {
				availablesIP.add(availableIP);
				availableIP = availableIP.addOperation(oneIP);
			}
		}

		return availablesIP;
	}

	/**
	 * Arma la direccion de red a partir del binNumber
	 * 
	 * @param binNumber
	 * @return redAddressIP
	 * @throws Exception
	 */
	private IP buildRedAddressIP(BinaryNumber binNumber, String binNumberAfter)
			throws Exception {

		String binNumberS = binNumber.getBinNumber();

		// Creo la cantidad de 0 (ceros) que faltan adelante
		StringBuffer zerosBefore = new StringBuffer();
		for (int i = 0; i < 24; i++) {
			zerosBefore.append("0");
		}

		// Creo la cantidad de 0 (ceros) que faltan al final
		StringBuffer zerosAfter = null;
		if (binNumberS.length() < 8) {
			zerosAfter = new StringBuffer();
			for (int i = 0; i < 8 - binNumberS.length(); i++) {
				zerosAfter.append(binNumberAfter);
			}
			binNumberS = binNumberS + zerosAfter.toString();
		}

		// Armo el binario completo
		binNumberS = zerosBefore.toString() + binNumberS
				+ zerosAfter.toString();

		// Agrego los "." a la direccion IP
		StringBuffer ipFixed = new StringBuffer();
		int bits = 0;
		for (int i = 0; i < 32; i++) {
			ipFixed.append(binNumberS.charAt(i));
			if (bits == 7) {
				ipFixed.append(".");
				bits = 0;
			} else
				bits++;
		}

		// Agrego el binario a la lista de redes permitidas
		IP redAddressIP = this.redAddressIP.addOperation(new IP(ipFixed
				.toString(), IPFormat.BINARY));

		return redAddressIP;
	}

	/**
	 * Retorna las distintas combinaciones binarias usando bitsCant de bits
	 * 
	 * @param bitsCant
	 * @return binCombinations
	 */
	private List<BinaryNumber> getBinCombinations(int bitsCant) {

		BinaryNumber binNumber = new BinaryNumber(bitsCant);
		List<BinaryNumber> binCombNumbers = new ArrayList<BinaryNumber>();

		// Agrego en una lista las distintas combinaciones de bitsCant, desde
		// 000...0 hasta 111...1
		for (int i = 0; i < (int) Math.pow(2, bitsCant); i++) {

			// Creo un BinaryNumber con bitsCant y el valor i (decimal)
			BinaryNumber anotherBinNumber = new BinaryNumber(bitsCant, Integer
					.toString(i));

			// Lo sumo a binNumber
			BinaryNumber sumBinNumber = binNumber
					.addOperation(anotherBinNumber);

			// Lo agrego a la lista
			binCombNumbers.add(sumBinNumber);
		}

		return binCombNumbers;
	}

	/**
	 * Valida si la mascara es valida
	 * 
	 * @param mask
	 * @return validMask
	 * @throws Exception
	 */
	private boolean isValidMask(String maskS) throws Exception {

		// Creo IP para la mascara
		this.mask = new IP(maskS, IPFormat.DECIMAL);

		// Agrego las direcciones IP invalidas
		List<IP> invalidIPs = new ArrayList<IP>();
		invalidIPs.add(new IP("11111111.11111111.10000000.11111111",
				IPFormat.BINARY));
		invalidIPs.add(new IP("00000000.11111111.11111111.11111111",
				IPFormat.BINARY));

		// Verifico si la mascara es igual a algunas de las direcciones IP
		// invalidas
		boolean validMask = true;
		for (IP invalidIP : invalidIPs) {
			if (mask.equals(invalidIP)) {
				validMask = false;
				break;
			}
		}

		return validMask;
	}

	/**
	 * Retorna las direcciones de red
	 * 
	 * @return redAddressesIP
	 */
	public List<IP> getRedAddressesIP() {
		return this.redAddressesIP;
	}

	/**
	 * Retorna las direcciones broadcast
	 * 
	 * @return broadcastsIP
	 */
	public List<IP> getBroadcastsIP() {
		return this.broadcastsIP;
	}

	/**
	 * Retorna las direcciones IP disponibles
	 * 
	 * @return availablesIP
	 */
	public List<IP> getAvailablesIP() {
		return availablesIP;
	}

	/**
	 * Retorna la cantidad de direcciones IP disponibles
	 * 
	 * @return availablesIPCount
	 */
	public int getAvailablesIPCount() {

		if (availablesIP != null)
			return availablesIP.size();

		int zeroCount = mask.getZeroCount();

		// Calculo 2(cant ceros) - 2
		// Devuelve la cantidad de hosts en cada subred
		int availableIPs = ((int) Math.pow(2, zeroCount)) - 2;

		// Multiplico por la cantidad de subredes
		availableIPs = availableIPs * this.redAddressesIP.size();

		return availableIPs;
	}

	/**
	 * Retorna si la mascara es valida
	 * 
	 * @return validMask
	 */
	public boolean isValidMask() {
		return validMask;
	}

}
